package request

import (
	"context"
	"encoding/json"
	"math"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"gitee.com/arjunxw/iothub-south-client/config"
	"gitee.com/arjunxw/iothub-south-client/service"
	"gitee.com/arjunxw/iothub-south-client/util"
	"github.com/google/wire"
	"go.uber.org/zap"
)

type Request interface {
	Auth(ctx context.Context, dalId string) *AuthResult
	RefreshToken(ctx context.Context, dalId string, refreshToken string) *AuthResult
	BindApp(ctx context.Context, dalId, productKey, appId, sn string) (err error)
	SetCallbackUrl(ctx context.Context, dalId, serverUrl string) (err error)
	ReportCmdResultWithoutParams(ctx context.Context, dalId, productKey, sn, messageId, result string, time time.Time, remark string) (err error)
	ReportCmdResult(ctx context.Context, dalId, productKey, sn, messageId, result string, time time.Time, remark string, states map[string]interface{}) (err error)
	ReportStatus(ctx context.Context, dalId, productKey, sn string, states map[string]interface{}, time time.Time) (err error)
	ReportEvent(ctx context.Context, dalId, productKey, sn, reportId, eventCode string, params map[string]interface{}, time time.Time) (err error)
	ReportUpgradeProgress(ctx context.Context, dalId, productKey, sn, upgradeId, status string, progress int, remark string, time time.Time) (err error)
	ReportGwTopology(ctx context.Context, dalId string, gwTopology *GwTopology) (err error)
	ReportSerialNet(ctx context.Context, dalId, productKey, sn, reportId, data string, time time.Time, params map[string]interface{}) (err error)
}

var ProviderSet = wire.NewSet(NewRequest)

type request struct {
	host           string
	dalInfoService service.DalInfoService
	reportService  service.ReportService
	log            *zap.Logger
	add            chan *service.RetryRecord
	rem            chan *service.RetryRecord
}

func NewRequest(cfg config.Config, log *zap.Logger, dalInfoService service.DalInfoService, reportService service.ReportService) Request {
	return &request{
		host:           cfg.GetHost(),
		log:            log,
		dalInfoService: dalInfoService, reportService: reportService,
		add: make(chan *service.RetryRecord), rem: make(chan *service.RetryRecord)}
}

func (req *request) Init() error {
	ctx := context.Background()
	count, records := req.reportService.FindRetryPage(ctx, 1, 100)
	appendFunc := func(retryRecords []*service.RetryRecord) {
		if len(retryRecords) > 0 {
			records = append(records, retryRecords...)
		}
	}
	appendFunc(records)
	page := int(math.Ceil(float64(count) / 100.0))
	for i := 2; i <= page; i++ {
		_, rs := req.reportService.FindRetryPage(ctx, i, 100)
		appendFunc(rs)
	}

	go req.run(records)
	return nil
}

func (req *request) Auth(ctx context.Context, dalId string) *AuthResult {
	return dalInfoToAuthResult(req.getDalInfo(ctx, dalId))
}

func (req *request) RefreshToken(ctx context.Context, dalId string, refreshToken string) *AuthResult {
	return dalInfoToAuthResult(req.refreshToken(ctx, dalId, refreshToken))
}

func (req *request) BindApp(ctx context.Context, dalId, productKey, appId, sn string) (err error) {
	dalInfo := req.getDalInfo(ctx, dalId)
	bindUrl := BindAppUrl(req.host, productKey, sn, appId)
	respBody := ResponseBody{}
	err = util.HttpPostFormHeader(bindUrl, http.Header{"Authorization": {dalInfo.AccessToken}}, url.Values{}, &respBody)
	if err != nil {
		return
	}
	err = validateResponse(&respBody)
	if err != nil {
		req.log.Error("BindApp", zap.Error(err), zap.Any("responseBody", &respBody),
			zap.String("url", bindUrl), zap.String("productKey", productKey))
	}
	return
}

func (req *request) SetCallbackUrl(ctx context.Context, dalId, serverUrl string) (err error) {
	dalInfo := req.getDalInfo(ctx, dalId)
	subUrl := SubscribeUrl(req.host)
	respBody := ResponseBody{}
	err = util.HttpPostFormHeader(subUrl, http.Header{"Authorization": {dalInfo.AccessToken}}, url.Values{"serverUrl": {serverUrl}}, &respBody)
	if err != nil {
		return
	}
	err = validateResponse(&respBody)
	if err != nil {
		req.log.Error("SetCallbackUrl", zap.Error(err), zap.Any("responseBody", &respBody),
			zap.String("url", subUrl), zap.String("serverUrl", serverUrl))
	}
	return
}

func (req *request) ReportCmdResultWithoutParams(
	ctx context.Context, dalId, productKey, sn, messageId, result string, time time.Time, remark string) (err error) {
	return req.ReportCmdResult(ctx, dalId, productKey, sn, messageId, result, time, remark, nil)
}

func (req *request) ReportCmdResult(ctx context.Context, dalId, productKey, sn,
	messageId, result string, time time.Time, remark string, states map[string]interface{}) (err error) {
	dalInfo := req.getDalInfo(ctx, dalId)

	cmdResultUrl := ReportCmdResultUrl(req.host, productKey, sn)
	content := make(map[string]interface{})
	content["messageId"] = messageId
	content["result"] = result
	content["states"] = states
	content["time"] = time.Format("2006-01-02 15:04:05")
	content["remark"] = remark
	history := req.createHistory(ctx, util.UUID(), cmdResultUrl, productKey, sn, dalInfo.ID, content)

	respBody := ResponseBody{}
	err = util.HttpPostJsonHeader(cmdResultUrl, http.Header{"Authorization": {dalInfo.AccessToken}}, content, &respBody)
	if err == nil {
		err = validateResponse(&respBody)
	}
	if err != nil {
		req.log.Error("ReportCmdResult", zap.Error(err), zap.Any("responseBody", &respBody),
			zap.String("url", cmdResultUrl), zap.Any("content", content))
	}
	return req.handleResult(ctx, dalId, history, err)
}

func (req *request) ReportStatus(ctx context.Context, dalId, productKey, sn string, states map[string]interface{}, time time.Time) (err error) {
	dalInfo := req.getDalInfo(ctx, dalId)

	statusUrl := ReportStatusUrl(req.host, productKey, sn)
	content := make(map[string]interface{})
	content["reportId"] = util.UUID()
	content["states"] = states
	content["time"] = time.Format("2006-01-02 15:04:05")
	history := req.createHistory(ctx, content["reportId"].(string), statusUrl, productKey, sn, dalInfo.ID, content)

	respBody := ResponseBody{}
	err = util.HttpPostJsonHeader(statusUrl, http.Header{"Authorization": {dalInfo.AccessToken}}, content, &respBody)
	if err == nil {
		err = validateResponse(&respBody)
	}
	if err != nil {
		req.log.Error("ReportStatus", zap.Error(err), zap.Any("responseBody", &respBody),
			zap.String("url", statusUrl), zap.Any("content", content))
	}
	return req.handleResult(ctx, dalId, history, err)
}

func (req *request) ReportEvent(ctx context.Context, dalId, productKey, sn, reportId, eventCode string, params map[string]interface{}, time time.Time) (err error) {
	dalInfo := req.getDalInfo(ctx, dalId)
	eventUrl := ReportEventUrl(req.host, productKey, sn)

	if strings.TrimSpace(reportId) == "" {
		reportId = util.UUID()
	}
	content := make(map[string]interface{})
	content["reportId"] = reportId
	content["eventCode"] = eventCode
	content["params"] = params
	content["time"] = time.Format("2006-01-02 15:04:05")
	history := req.createHistory(ctx, reportId, eventUrl, productKey, sn, dalInfo.ID, content)

	respBody := ResponseBody{}
	err = util.HttpPostJsonHeader(eventUrl, http.Header{"Authorization": {dalInfo.AccessToken}}, content, &respBody)
	if err == nil {
		err = validateResponse(&respBody)
	}
	if err != nil {
		req.log.Error("ReportEvent", zap.Error(err), zap.Any("responseBody", &respBody),
			zap.String("url", eventUrl), zap.Any("content", content))
	}
	return req.handleResult(ctx, dalId, history, err)
}

func (req *request) ReportUpgradeProgress(ctx context.Context, dalId, productKey, sn, upgradeId, status string, progress int, remark string, time time.Time) (err error) {
	dalInfo := req.getDalInfo(ctx, dalId)

	upgradeProgressUrl := ReportUpgradeProgressUrl(req.host, productKey, sn)
	content := url.Values{
		"upgradeId": {upgradeId},
		"status":    {status},
		"progress":  {strconv.Itoa(progress)},
		"remark":    {remark},
		"time":      {time.Format("2006-01-02 15:04:05")},
	}

	respBody := ResponseBody{}
	err = util.HttpPostFormHeader(upgradeProgressUrl, http.Header{"Authorization": {dalInfo.AccessToken}}, content, &respBody)
	if err != nil {
		return
	}
	err = validateResponse(&respBody)
	if err != nil {
		req.log.Error("ReportUpgradeProgress", zap.Error(err), zap.Any("responseBody", &respBody),
			zap.String("url", upgradeProgressUrl), zap.Any("content", content))
	}
	return
}

func (req *request) ReportGwTopology(ctx context.Context, dalId string, gwTopology *GwTopology) (err error) {
	dalInfo := req.getDalInfo(ctx, dalId)

	gwUrl := ReportGwTopologyUrl(req.host)
	respBody := ResponseBody{}
	err = util.HttpPostJsonHeader(gwUrl, http.Header{"Authorization": {dalInfo.AccessToken}}, gwTopology, &respBody)
	if err != nil {
		return
	}
	err = validateResponse(&respBody)
	if err != nil {
		req.log.Error("ReportGwTopology", zap.Error(err), zap.Any("responseBody", &respBody),
			zap.String("url", gwUrl), zap.Any("content", gwTopology))
	}
	return
}

func (req *request) ReportSerialNet(ctx context.Context, dalId, productKey, sn, reportId, data string, time time.Time, params map[string]interface{}) (err error) {
	dalInfo := req.getDalInfo(ctx, dalId)

	eventUrl := ReportEventUrl(req.host, productKey, sn)
	content := make(map[string]interface{})
	content["reportId"] = reportId
	content["eventCode"] = "SerialNet"
	content["time"] = time.Format("2006-01-02 15:04:05")
	if params == nil {
		params = make(map[string]interface{}, 1)
	}
	params["data"] = data
	content["params"] = params

	respBody := ResponseBody{}
	err = util.HttpPostJsonHeader(eventUrl, http.Header{"Authorization": {dalInfo.AccessToken}}, content, &respBody)
	if err != nil {
		return
	}
	err = validateResponse(&respBody)
	if err != nil {
		req.log.Error("ReportSerialNet", zap.Error(err), zap.Any("responseBody", &respBody),
			zap.String("url", eventUrl), zap.Any("content", content))
	}
	return
}

func (req *request) createHistory(ctx context.Context,
	reportId, url, productKey, sn string, dalId int64, content map[string]interface{}) *service.ReportHistory {
	bytes, _ := json.Marshal(content)
	history := &service.ReportHistory{
		ReportId:   reportId,
		Url:        url,
		ProductKey: productKey,
		Sn:         sn,
		DalInfoId:  dalId,
		Content:    string(bytes),
		Status:     service.Pending,
		RetryTimes: 0,
	}
	if err := req.reportService.CreateHistory(ctx, history); err != nil {
		return nil
	}
	return history
}
